SPB Git forge
15commits 1branches 0releases
29.7 MBsize
maindefault branch
10 days agolast push
TypeScript 36.3% Python 31.8% Go 18% JavaScript 9.8% Shell 1.9% SQL 1.4% CSS 0.5%
10.9 KB · 245 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { IncidentSeriesChart } from '@/components/incidents/IncidentSeriesChart';4import { TYPE_LABEL } from '@/components/incidents/IncidentRow';5import { Bar, ConfBar, Section, Stat, StatusDot } from '@/components/ui/primitives';6import { apiGet, apiTry } from '@/lib/api';7import { fmt, fmtDuration, fmtInt, fmtRatio } from '@/lib/format';8import { pressureColor } from '@/lib/pressure';9import { Time } from '@/lib/time';10import type { IncidentDetail } from '@/lib/types';1112export const dynamic = 'force-dynamic';1314type Params = Promise<{ slug: string }>;1516export async function generateMetadata({ params }: { params: Params }): Promise<Metadata> {17  const { slug } = await params;18  const i = await apiTry<IncidentDetail>(`/api/v1/incident/${encodeURIComponent(slug)}`);19  if (!i) return { title: 'Incident' };20  return { title: `${i.title} — ${i.status}, peak ${fmt(i.peak_pressure)}`, description: i.summary, alternates: { canonical: `/event/${i.slug}` }, openGraph: { type: 'article', publishedTime: i.started_at, modifiedTime: i.updated_at } };21}2223function scopeHref(i: IncidentDetail): string | null {24  if (i.scope_type === 'region' && i.scope_id) return `/internet/${i.scope_id}`;25  if (i.scope_type === 'country' && i.scope_id) return `/country/${i.scope_id.toLowerCase()}`;26  if (i.scope_type === 'asn' && i.scope_id) return `/asn/${i.scope_id}`;27  if (i.scope_type === 'service' && i.scope_id) return `/service/${i.scope_id}`;28  return null;29}3031export default async function EventPage({ params }: { params: Params }) {32  const { slug } = await params;33  const i = await apiGet<IncidentDetail>(`/api/v1/incident/${encodeURIComponent(slug)}`);34  const href = scopeHref(i);35  return (36    <article className="pb-8">37      <header className="grid gap-6 pt-6 pb-5 md:grid-cols-[minmax(0,1fr)_auto] md:items-end">38        <div className="min-w-0">39          <p className="label flex flex-wrap items-center gap-3">40            <span>{TYPE_LABEL[i.type] ?? i.type}</span>41            <StatusDot status={i.status} />42            <span className="num normal-case tracking-normal text-ink-3">{i.event_id}</span>43          </p>44          <h1 className="mt-1 text-[26px] font-medium leading-tight tracking-tight sm:text-[32px]">{i.title}</h1>45          <p className="mt-2 max-w-[760px] text-[14px] text-ink-2">{i.summary}</p>46          <p className="num mt-3 flex flex-wrap gap-x-5 gap-y-1 text-[12px] text-ink-2">47            <span>48              scope{' '}49              {href ? (50                <Link href={href} className="text-ink hover:text-accent">51                  {i.scope_label}52                </Link>53              ) : (54                <span className="text-ink">{i.scope_label}</span>55              )}56            </span>57            <span>58              started <Time ts={i.started_at} className="text-ink" />59            </span>60            <span>61              {i.ended_at ? (62                <>63                  ended <Time ts={i.ended_at} className="text-ink" />64                </>65              ) : (66                <>67                  updated <Time ts={i.updated_at} className="text-ink" />68                </>69              )}70            </span>71            <span>72              duration <span className="text-ink">{fmtDuration(i.duration_s)}</span>73            </span>74          </p>75        </div>76        <div className="flex flex-wrap items-end gap-6">77          <div>78            <div className="label">current</div>79            <div className="num text-[56px] leading-none tracking-[-0.03em]" style={{ color: pressureColor(i.current_pressure) }}>80              {fmt(i.current_pressure)}81            </div>82          </div>83          <div>84            <div className="label">peak</div>85            <div className="num text-[28px] leading-none" style={{ color: pressureColor(i.peak_pressure) }}>86              {fmt(i.peak_pressure)}87            </div>88          </div>89          <div className="pb-1">90            <div className="label mb-1">confidence</div>91            <ConfBar value={i.confidence} />92          </div>93        </div>94      </header>9596      <Section label="Series" right={<span>incident scope vs global pressure · from 30 min before detection</span>}>97        <IncidentSeriesChart series={i.series} timeline={i.timeline} />98      </Section>99100      <div className="grid grid-cols-[minmax(0,1fr)] gap-x-10 lg:grid-cols-[minmax(0,2fr)_minmax(0,3fr)]">101        <Section label="Timeline" className="min-w-0">102          <ol className="relative ml-2 border-l border-line">103            {i.timeline.map((t, idx) => (104              <li key={idx} className="relative mb-4 pl-5 last:mb-0">105                <span className="absolute -left-[5px] top-1.5 size-2 rounded-full border border-bg" style={{ background: pressureColor(t.pressure) }} aria-hidden="true" />106                <div className="flex flex-wrap items-baseline gap-x-3">107                  <StatusDot status={t.status} />108                  <Time ts={t.ts} className="num text-[11.5px] text-ink-2" />109                  <span className="num text-[12px]" style={{ color: pressureColor(t.pressure) }}>110                    {fmt(t.pressure)}111                  </span>112                </div>113                <p className="mt-0.5 text-[12.5px] text-ink-2">{t.note}</p>114              </li>115            ))}116          </ol>117        </Section>118        <Section label="Hypotheses" right={<span>never overstated · confidence-weighted</span>} className="min-w-0">119          {i.hypotheses.length ? (120            <ul className="space-y-4">121              {i.hypotheses.map((h, idx) => (122                <li key={idx}>123                  <div className="flex items-baseline justify-between gap-4">124                    <p className="text-[14px] text-ink">{h.text}</p>125                    <span className="num shrink-0 text-[12px] text-ink-2">{Math.round(h.confidence * 100)} %</span>126                  </div>127                  <Bar value={h.confidence * 100} color="var(--accent)" className="mt-1.5" />128                  <ul className="mt-2 space-y-0.5 text-[12.5px] text-ink-2">129                    {h.evidence.map((e, k) => (130                      <li key={k}>· {e}</li>131                    ))}132                  </ul>133                </li>134              ))}135            </ul>136          ) : (137            <p className="text-[12.5px] text-ink-3">No causal hypothesis reached the minimum confidence.</p>138          )}139        </Section>140      </div>141142      <Section label="Evidence" right={<span>signal · scope · current vs baseline · robust z</span>}>143        <div className="scroll-x -mx-3 px-3">144          <table className="tbl">145            <thead>146              <tr>147                <th>Signal</th>148                <th className="hidden sm:table-cell">Scope</th>149                <th className="r">Current</th>150                <th className="r">Baseline</th>151                <th className="r">z</th>152                <th className="r hidden sm:table-cell">Samples</th>153                <th className="r hidden md:table-cell">At</th>154              </tr>155            </thead>156            <tbody>157              {i.evidence.map((e, idx) => (158                <tr key={idx}>159                  <td>160                    <span className="text-ink">{e.label}</span> <span className="num text-[10.5px] text-ink-3">{e.signal_id}</span>161                  </td>162                  <td className="hidden text-ink-2 sm:table-cell">{e.scope_id ? `${e.scope_type}:${e.scope_id}` : e.scope_type}</td>163                  <td className="num r">{fmt(e.current, Math.abs(e.current) < 1 ? 3 : 1)}</td>164                  <td className="num r text-ink-2">{fmt(e.baseline, Math.abs(e.baseline) < 1 ? 3 : 1)}</td>165                  <td className="num r" style={{ color: Math.abs(e.robust_z) >= 3 ? 'var(--p-high)' : 'var(--ink)' }}>166                    {fmt(e.robust_z)}167                  </td>168                  <td className="num r hidden text-ink-2 sm:table-cell">{fmtInt(e.samples)}</td>169                  <td className="num r hidden text-ink-2 md:table-cell">170                    <Time ts={e.ts} style="time" />171                  </td>172                </tr>173              ))}174            </tbody>175          </table>176        </div>177        {i.bgp && (178          <div className="mt-4 grid grid-cols-3 gap-4 sm:max-w-[480px]">179            <Stat label="BGP withdrawals" value={fmtRatio(i.bgp.withdrawals_ratio)} sub="vs baseline" />180            <Stat label="BGP announcements" value={fmtRatio(i.bgp.announcements_ratio)} sub="vs baseline" />181            <Stat label="origin changes" value={fmtInt(i.bgp.origin_changes)} />182          </div>183        )}184      </Section>185186      <div className="grid grid-cols-[minmax(0,1fr)] gap-x-10 lg:grid-cols-2">187        <Section label={`Affected probes · ${fmtInt(i.affected_probes)}`} className="min-w-0">188          <ul className="divide-y divide-line text-[12.5px]">189            {i.probes.map((p) => (190              <li key={p.probe_id} className="flex flex-wrap gap-x-3 py-1.5">191                <span className="num w-24 text-ink">{p.probe_id}</span>192                <Link href={`/internet/${p.region}`} className="w-24 text-ink-2 hover:text-accent">193                  {p.region}194                </Link>195                <span className="flex-1 text-ink-2">{p.observation}</span>196              </li>197            ))}198          </ul>199        </Section>200        <Section label={`Affected targets · ${fmtInt(i.affected_targets)}`} className="min-w-0">201          <ul className="divide-y divide-line text-[12.5px]">202            {i.targets.map((t) => (203              <li key={t.target_id} className="flex flex-wrap gap-x-3 py-1.5">204                <Link href={`/service/${t.service_id}`} className="w-40 truncate text-ink hover:text-accent">205                  {t.name}206                </Link>207                <span className="flex-1 text-ink-2">{t.observation}</span>208              </li>209            ))}210          </ul>211          <div className="mt-3 flex flex-wrap gap-1.5">212            {i.affected_asns.map((a) => (213              <Link key={a} href={`/asn/${a}`} className="num rounded-[3px] border border-line px-1.5 py-0.5 text-[11px] text-ink hover:border-line-2">214                AS{a}215              </Link>216            ))}217            {i.affected_services.map((s) => (218              <Link key={s} href={`/service/${s}`} className="rounded-[3px] border border-line px-1.5 py-0.5 text-[11px] text-ink hover:border-line-2">219                {s}220              </Link>221            ))}222          </div>223        </Section>224      </div>225226      <Section label="Annotations">227        {i.annotations.length ? (228          <ul className="divide-y divide-line text-[13px]">229            {i.annotations.map((a, idx) => (230              <li key={idx} className="py-2">231                <p className="text-ink">{a.text}</p>232                <p className="num mt-0.5 text-[11px] text-ink-3">233                  {a.author} · <Time ts={a.ts} />234                </p>235              </li>236            ))}237          </ul>238        ) : (239          <p className="text-[12.5px] text-ink-3">No human annotation yet.</p>240        )}241      </Section>242    </article>243  );244}245